package com.starview.star.compiler.sources;

import com.starview.platform.data.type.Location;
import com.starview.platform.data.value.ResourceURI;
import com.starview.platform.resource.ResourceException;
import com.starview.platform.resource.Resources;
import com.starview.platform.resource.URIUtils;
import com.starview.platform.resource.catalog.Catalog;
import com.starview.platform.resource.catalog.CatalogException;
import com.starview.platform.resource.catalog.CatalogUtils;
import com.starview.platform.resource.catalog.URIBasedCatalog;
import com.starview.star.code.repository.CodeRepository;
import com.starview.star.compiler.ErrorReport;
import com.starview.star.compiler.grammar.Token;
import com.starview.star.compiler.grammar.Token.TokenType;
import com.starview.star.compiler.grammar.Tokenizer;
import com.starview.star.compiler.standard.StandardNames;
import com.starview.star.compiler.util.PrettyPrintDisplay;
import com.starview.star.compiler.util.PrettyPrintable;
import com.starview.star.compiler.util.TopologySort;
import com.starview.star.compiler.util.TopologySort.IDefinition;

import java.io.IOException;
import java.io.Reader;
import java.util.*;
import java.util.Map.Entry;

@SuppressWarnings("serial")
public class PackageGrapher implements PrettyPrintable{
  // functions to build a dependency graph of which packages depend on which

  private final Map<ResourceURI, PkgGraph> graph = new HashMap<>();
  private final ErrorReport errors = new ErrorReport();
  private final CodeRepository repository;

  private PackageGrapher(CodeRepository repository){
    this.repository = repository;
  }

  public static PackageGrapher buildImportGraph(ResourceURI pkg, CodeRepository repository, Catalog catalog){
    PackageGrapher grapher = new PackageGrapher(repository);

    try {
      grapher.processGraph(pkg, catalog);
    } catch (ResourceException e) {
      e.printStackTrace();
    }

    return grapher;
  }

  public static PackageGrapher buildImportGraph(Collection<ResourceURI> pkgs, CodeRepository repository,
                                                Catalog catalog)
          throws ResourceException{
    PackageGrapher grapher = new PackageGrapher(repository);

    for (ResourceURI pkg : pkgs)
      grapher.processGraph(pkg, catalog);

    return grapher;
  }

  public List<List<PkgSpec>> sortDependencies(){
    List<List<IDefinition<ResourceURI>>> groups = new TopologySort<>(graph.values()).sort();
    List<List<PkgSpec>> sortd = new ArrayList<>();
    for (List<IDefinition<ResourceURI>> entry : groups) {
      List<PkgSpec> group = new ArrayList<>();
      for (IDefinition<ResourceURI> def : entry) {
        PkgGraph pkgGraph = (PkgGraph) def;
        group.add(new PkgSpec(pkgGraph.pkg, pkgGraph.getCatalog(), pkgGraph.links, pkgGraph.getReferences()));
      }
      sortd.add(group);
    }
    return sortd;
  }

  public Map<ResourceURI, List<ResourceURI>> linkMap(){
    Map<ResourceURI, List<ResourceURI>> links = new HashMap<>();

    for (Entry<ResourceURI, PkgGraph> entry : graph.entrySet()) {
      links.put(entry.getKey(), entry.getValue().getLinks());
    }
    return links;
  }

  private class PkgGraph implements IDefinition<ResourceURI>, PrettyPrintable{
    private final ResourceURI pkg;
    private final Catalog catalog;
    private final List<ResourceURI> references = new ArrayList<>();
    private final List<ResourceURI> links = new ArrayList<>();

    public PkgGraph(ResourceURI pkg, Catalog catalog){
      this.pkg = pkg;
      this.catalog = catalog;

      assert !graph.containsKey(pkg);
      graph.put(pkg, this);
    }

    private void processPkg(Catalog catalog, CodeRepository repository) throws ResourceException{

      try {
        Reader rdr = Resources.getReader(pkg);
        Tokenizer tokenizer = new Tokenizer(new ErrorReport.NullErrorReporter(), rdr,
                Location.location(pkg));
        try {
          Token tok = tokenizer.nextToken();
          while (tok.getType() != TokenType.terminal) {
            if (tok.isIdentifier(StandardNames.IMPORT)) {
              tok = tokenizer.nextToken();

              try {
                final ResourceURI pkgRef;

                switch (tok.getType()) {
                  case identifier:
                    pkgRef = catalog.resolve(tok.getImage());
                    break;
                  case string:
                    pkgRef = catalog.resolve(URIUtils.parseUri(tok.getImage()));
                    break;
                  default:
                    pkgRef = null;
                }
                if (pkgRef != null) {
                  if (!references.contains(pkgRef)) {
                    references.add(pkgRef);
                    Catalog refCatalog = lookForCatalog(pkgRef, catalog, repository);
                    processGraph(pkgRef, refCatalog, pkg);
                  }
                }
              } catch (CatalogException e) {
              }
            }
            tok = tokenizer.nextToken();
          }
        } finally {
          rdr.close();
        }
      } catch (ResourceException e) {
        errors.reportError("problem in accessing resource " + e.getMessage(), Location.location(pkg));
      } catch (IOException e) {
        errors.reportError("IO problem in accessing resource " + pkg + ": " + e.getMessage(), Location.location(pkg));
      }
    }

    private void linkFromReferringPkg(ResourceURI referringPkg){
      if (!links.contains(referringPkg))
        links.add(referringPkg);
    }

    public List<ResourceURI> getLinks(){
      return links;
    }

    public List<ResourceURI> getReferences(){
      return references;
    }

    public Catalog getCatalog(){
      return catalog;
    }

    @Override
    public boolean defines(ResourceURI uri){
      return pkg.equals(uri);
    }

    @Override
    public Collection<ResourceURI> references(){
      return references;
    }

    @Override
    public void prettyPrint(PrettyPrintDisplay disp){
      pkg.prettyPrint(disp);
      if (!references.isEmpty()) {
        disp.append("->");
        disp.prettyPrint(references, ", ");
        disp.append(".");
      }
    }
  }

  private void processGraph(ResourceURI pkg, Catalog catalog, ResourceURI referringPkg) throws ResourceException{
    if (pkg != null) {
      if (!graph.containsKey(pkg)) {
        PkgGraph pkgGrapher = new PkgGraph(pkg, catalog);

        pkgGrapher.processPkg(catalog, repository);
        pkgGrapher.linkFromReferringPkg(referringPkg);
      } else
        graph.get(pkg).linkFromReferringPkg(referringPkg);
    }
  }

  private void processGraph(ResourceURI pkg, Catalog catalog) throws ResourceException{
    if (pkg != null) {
      if (!graph.containsKey(pkg)) {
        PkgGraph pkgGrapher = new PkgGraph(pkg, catalog);

        pkgGrapher.processPkg(catalog, repository);
      }
    }
  }

  public static Catalog lookForCatalog(ResourceURI uri, Catalog fallback, CodeRepository repository){
    try {
      return CatalogUtils.parseCatalog(uri.resolve(Catalog.CATALOG), fallback);
    } catch (Exception e) {
      return new URIBasedCatalog(uri, fallback);
    }
  }

  @Override
  public void prettyPrint(PrettyPrintDisplay disp){
    for (Entry<ResourceURI, PkgGraph> entry : graph.entrySet()) {
      entry.getKey().prettyPrint(disp);
      disp.append(": ");
      disp.prettyPrint(entry.getValue().references(), ", ");
      disp.append("\n");
    }
  }

  @Override
  public String toString(){
    return PrettyPrintDisplay.toString(this);
  }
}
